/*
   Copyright 2009 Ryan Phillips

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
#include <apr.h>
#include <apr_network_io.h>
#include <apr_poll.h>
#include <apr_buckets.h>
#include <apr_strings.h>
#include <apr_getopt.h>
#include <apr_thread_proc.h>
#include <apr_signal.h>
#include <apr_global_mutex.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#include <sys/inotify.h>
#include "remotecores.h"

/* size of the event structure, not counting name */
#define EVENT_SIZE (sizeof (struct inotify_event))

/* reasonable guess as to size of 1024 events */
#define BUF_LEN (1024 * (EVENT_SIZE + 16))

typedef struct
{
    apr_pool_t *pool;
    char *host;
    apr_port_t port;
    apr_file_t *errfile;
    apr_int32_t remove_after_transmission;
    apr_global_mutex_t *lock;
} rc_config_t;

static void watch_for_cores(rc_config_t * config, const char *path);

static void *APR_THREAD_FUNC admin_thread(apr_thread_t * tid, void *data);

static void usage(rc_config_t * config);

static const apr_getopt_option_t client_options[] = {
    {"remote", 'r', 1, "FQDN or IP of remote server"},
    {"port", 'p', 1, "Port of server"},
    {"dir", 'd', 1, "Directory to watch"},
    {"remove", 'f', 0, "Remove core after transmission"},
    {"help", 'h', 0, "help"},
    {0, 0, 0, 0},
};


int main(int argc, const char **argv)
{
    apr_getopt_t *os;
    apr_status_t rv;
    apr_threadattr_t *tattr;
    apr_thread_t *tid;
    int opt;
    const char *arg;
    const char *path = NULL;
    rc_config_t config;
    remotecores_error_t *err;

    /* Init */
    apr_initialize();
    apr_pool_create(&config.pool, NULL);
    apr_file_open_stderr(&config.errfile, config.pool);
    apr_global_mutex_create(&config.lock, "config.lock", APR_LOCK_DEFAULT,
                            config.pool);

    config.port = REMOTECORES_DEFAULT_PORT;
    config.host = NULL;

    /* Parse Commandline */
    rv = apr_getopt_init(&os, config.pool, argc, argv);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "error getting options");
        remotecores_error_destroy(err);
        return 0;
    }

    while (1) {
        rv = apr_getopt_long(os, client_options, &opt, &arg);
        if (APR_STATUS_IS_EOF(rv)) {
            break;
        }

        if (rv != APR_SUCCESS) {
            usage(&config);
        }

        switch (opt) {
        case 'r':
            config.host = apr_pstrdup(config.pool, arg);
            break;
        case 'p':
            config.port = atoi(arg);
            break;
        case 'd':
            path = arg;
            break;
        case 'f':
            config.remove_after_transmission = TRUE;
            break;
        case 'h':
            usage(&config);
            exit(1);
        }
    }

    if (path == NULL || config.host == NULL) {
        usage(&config);
    }

    apr_file_printf(config.errfile, "[%s:%d] Using Server (%s:%i)\n",
                    __FILE__, __LINE__, config.host, config.port);
    apr_file_printf(config.errfile, "[%s:%d] Watching Path (%s)\n",
                    __FILE__, __LINE__, path);

    apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);

    /* Create admin socket */
    apr_threadattr_create(&tattr, config.pool);
    apr_threadattr_detach_set(tattr, 0);
    apr_thread_create(&tid, tattr, admin_thread, &config, config.pool);

    /* Run */
    watch_for_cores(&config, path);

    /* Cleanup */
    apr_pool_destroy(config.pool);
    apr_terminate();

    return 0;
}

/* Return the fd for the inotify device */
static int init_watch(const char *path)
{
    int fd;
    int rv;

    fd = inotify_init();
    if (fd < 0) {
        perror("inotify_init");
    }

    rv = inotify_add_watch(fd, path, IN_CLOSE_WRITE);
    if (rv < 0) {
        perror("inotify_add_watch");
    }

    return fd;
}

static remotecores_error_t *
send_brigade(apr_socket_t * sock, apr_bucket_brigade * bb)
{
    apr_status_t rv;
    apr_bucket *bucket = APR_BRIGADE_FIRST(bb);
    while (bucket != APR_BRIGADE_SENTINEL(bb)) {
        const char *buffer;
        apr_size_t nbytes, o, i;

        apr_bucket_read(bucket, &buffer, &nbytes, APR_BLOCK_READ);

        o = 0;
        i = nbytes;
        while (i > 0) {
            nbytes = i;
            rv = apr_socket_send(sock, buffer + o, &nbytes);
            if (rv != APR_SUCCESS) {
                remotecores_error_t *err;
                err = remotecores_error_create(rv, "apr_socket_send");
                return err;
            }
            o += nbytes;
            i -= nbytes;
        }

        bucket = APR_BUCKET_NEXT(bucket);
    }

    return REMOTECORES_SUCCESS;
}

static remotecores_error_t *send_core(rc_config_t * config, const char *path,
                                      const char *filename)
{
    apr_uint32_t protocol_version = REMOTECORES_PROTOCOL_VERSION;
    apr_pool_t *t_pool;
    apr_socket_t *sock;
    apr_sockaddr_t *address;
    apr_finfo_t finfo;
    apr_file_t *file;
    apr_bucket_alloc_t *alloc;
    apr_bucket_brigade *out;
    remotecores_protocol_t *proto;
    char *filepath;
    apr_status_t rv;
    remotecores_error_t *err = REMOTECORES_SUCCESS;

    apr_pool_create(&t_pool, config->pool);

    apr_file_printf(config->errfile, "[%s:%d] Found Core (%s)\n", 
                    __FILE__, __LINE__, filename);

    filepath = apr_psprintf(t_pool, "%s/%s", path, filename);

    rv = apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP,
                           t_pool);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "Could not create socket");
        goto done;
    }

    apr_global_mutex_lock(config->lock);

    rv = apr_sockaddr_info_get(&address, config->host, APR_INET, config->port,
                               0, t_pool);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "Could not get address");
        goto done;
    }

    apr_global_mutex_unlock(config->lock);

    apr_socket_timeout_set(sock, apr_time_from_sec(5));
    rv = apr_socket_connect(sock, address);
    if (rv != APR_SUCCESS &&
        !APR_STATUS_IS_EAGAIN(rv) && !APR_STATUS_IS_EINPROGRESS(rv)) {
        err = remotecores_error_create(rv, "connection error");
        goto done;
    }

    alloc = apr_bucket_alloc_create(t_pool);
    rv = apr_file_open(&file, filepath, APR_READ, APR_FPROT_OS_DEFAULT,
                       t_pool);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "file open failed");
        goto done;
    }

    apr_file_info_get(&finfo, APR_FINFO_SIZE, file);

    /* Protocol - See protocol.c for the wire protocol
     *   Version 1:
     *     1
     *     filename
     *     core filesize
     */

    remotecores_protocol_create(&proto, sock, t_pool);
    remotecores_uint32_write(proto, &protocol_version);
    remotecores_string_write(proto, filename);
    remotecores_offset_write(proto, &finfo.size);

    /* Send Core */
    out = apr_brigade_create(t_pool, alloc);
    APR_BRIGADE_INSERT_TAIL(out,
                            apr_bucket_file_create(file,
                                                   0,
                                                   finfo.size,
                                                   t_pool, alloc));
    err = send_brigade(sock, out);
    apr_bucket_alloc_destroy(alloc);
    apr_file_close(file);

    if (config->remove_after_transmission) {
        apr_file_remove(filepath, t_pool);
    }

  done:
    apr_pool_destroy(t_pool);
    return err;
}

void watch_for_cores(rc_config_t * config, const char *path)
{
    int inotify_fd;
    int len;
    int i;
    char buf[BUF_LEN];

    inotify_fd = init_watch(path);

    while (1) {
        len = read(inotify_fd, buf, BUF_LEN);
        if (len < 0) {
            if (errno == EINTR) {
                continue;
            }
            else {
                perror("read");
            }
        }

        i = 0;
        while (i < len) {
            struct inotify_event *event;
            remotecores_error_t *err;

            event = (struct inotify_event *) &buf[i];
            if (event->len && strstr(event->name, "core")) {
                err = send_core(config, path, event->name);
                if (err) {
                    apr_file_printf(config->errfile,
                                    "[%s:%d] Failed to send core: %s (%d)"
                                    APR_EOL_STR, err->file, err->line,
                                    err->msg, err->err);
                    remotecores_error_destroy(err);
                }
            }

            i += EVENT_SIZE + event->len;
        }
    }

    close(inotify_fd);
}

static void *APR_THREAD_FUNC admin_thread(apr_thread_t * tid, void *data)
{
    apr_socket_t *sock, *usock;
    apr_sockaddr_t *sa;
    apr_status_t rv;
    remotecores_error_t *err = REMOTECORES_SUCCESS;
    apr_pool_t *pool;
    rc_config_t *config = (rc_config_t *) data;

    apr_pool_create(&pool, NULL);

    rv = apr_sockaddr_info_get(&sa, "0.0.0.0", APR_INET,
                               REMOTECORES_DEFAULT_ADMIN_PORT, 0, pool);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "apr_sockaddr_info_get");
        apr_file_printf(config->errfile, "[%s:%d] %s (%d)" APR_EOL_STR,
                        err->file, err->line, err->msg, err->err);
        return NULL;
    }

    rv = apr_socket_create(&sock, APR_INET, SOCK_STREAM, APR_PROTO_TCP, pool);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "apr_socket_create");
        apr_file_printf(config->errfile, "[%s:%d] %s (%d)" APR_EOL_STR,
                        err->file, err->line, err->msg, err->err);
        return NULL;
    }

    apr_socket_opt_set(sock, APR_SO_REUSEADDR, 1);

    rv = apr_socket_bind(sock, sa);
    if (rv != APR_SUCCESS) {
        err = remotecores_error_create(rv, "apr_socket_bind");
        apr_file_printf(config->errfile, "[%s:%d] %s (%d)" APR_EOL_STR,
                        err->file, err->line, err->msg, err->err);
        return NULL;
    }

    apr_socket_listen(sock, 1);

    apr_signal(SIGPIPE, SIG_IGN);

    do {
        apr_pool_t *t_pool;
        apr_bucket_alloc_t *ba;
        apr_bucket_brigade *bbmain;
        apr_bucket_brigade *bbtmp;
        char buf[100];
        apr_size_t len;

        apr_pool_create(&t_pool, pool);

        rv = apr_socket_accept(&usock, sock, t_pool);

        ba = apr_bucket_alloc_create(t_pool);
        bbmain = apr_brigade_create(t_pool, ba);
        bbtmp = apr_brigade_create(t_pool, ba);
        APR_BRIGADE_INSERT_TAIL(bbmain, apr_bucket_socket_create(usock, ba));

        len = sizeof(buf);
        rv = apr_brigade_split_line(bbtmp, bbmain, APR_NONBLOCK_READ, len);
        if (rv == APR_EOF) {
            goto hangup;
        }

        if (rv != APR_SUCCESS) {
            err = remotecores_error_create(rv, "apr_brigade_split");
            apr_file_printf(config->errfile, "[%s:%d] %s (%d)" APR_EOL_STR,
                            err->file, err->line, err->msg, err->err);
            goto hangup;
        }

        rv = apr_brigade_flatten(bbtmp, buf, &len);
        if (rv != APR_SUCCESS) {
            err = remotecores_error_create(rv, "apr_brigade_flatten");
            apr_file_printf(config->errfile, "[%s:%d] %s (%d)" APR_EOL_STR,
                            err->file, err->line, err->msg, err->err);
            goto hangup;
        }

        /* Null Terminate */
        buf[len] = 0;


        apr_global_mutex_lock(config->lock);
        config->host = apr_pstrdup(config->pool, buf);
        apr_global_mutex_unlock(config->lock);

        apr_file_printf(config->errfile,
                        "[INFO] Destination IP changed to %s", config->host);

      hangup:
        apr_socket_close(usock);
        apr_pool_destroy(t_pool);
    } while (1);

    return NULL;
}

#define NL APR_EOL_STR

static void usage(rc_config_t * config)
{
    apr_file_printf(config->errfile,
    "[%s]"  NL
    "Usage: remotecore-client -r hostname --dir /path/to/directory [-p port] [-f]" NL
    "options:" NL
    "   -f           delete core after transmission" NL 
    "   -p           remote port (default: %i)" NL 
    "   -r           remote host (IP or FQDN)" NL
    "   --dir [path] directory to watch" NL
    ,REMOTECORES_VERSION_STRING
    ,REMOTECORES_DEFAULT_PORT);
    exit(EXIT_SUCCESS);
}
